import json
import time
import hmac
import base64
import hashlib
import requests
import smtplib
import pysyslogclient
import subprocess
from urllib import parse
from typing import List
from email import utils
from email.header import Header
from email.mime.text import MIMEText


class NotifyType:
    Type = ''


class DingTalk(NotifyType):
    """钉钉通知"""

    Type = 'dingtalk'

    def __init__(self, **kwargs):
        self.webhook_url: str = ''
        self.app_secret: str = ''
        self.to_user: List[str] = []  # @某几个人
        self.to_all: bool = False  # 发送给所有人
        if kwargs:
            self.init(**kwargs)

    def init(self, webhook_url, app_secret, to_user, to_all):
        self.webhook_url = webhook_url
        self.app_secret = app_secret
        self.to_user = to_user
        self.to_all = to_all

    def _construct_url(self):
        """app_secret需要加签名
        """
        url = self.webhook_url

        if self.app_secret:  # 需要加密签名
            timestamp = '%d' % (time.time() * 1000)
            sign = f'{timestamp}\n{self.app_secret}'
            hash_ = hmac.new(self.app_secret.encode(), msg=sign.encode(), digestmod=hashlib.sha256)
            sign = parse.quote(base64.b64encode(hash_.digest()), safe='')
            url = f'{url}&sign={sign}&timestamp={timestamp}'
        return url

    def notify(self, title='', content=''):
        url = self._construct_url()

        body = {
            'msgtype': 'text',
            'text': {'content': content},
            'at': {'atMobiles': self.to_user,
                   'isAtAll': self.to_all,
                   }
        }

        resp = requests.post(url, json=body, headers={'Content-Type': 'application/json;charset=utf-8'}, verify=False)
        if resp.json().get('errcode') != 0:
            errmsg = resp.json().get('errmsg', '')
            raise Exception(errmsg)

    def verify(self, title='', content=''):
        title = title or '钉钉通告'
        content = content or '钉钉通告验证'
        return self.notify(title=title, content=content)


class Email(NotifyType):
    """邮件通告"""

    Type = 'email'

    def __init__(self, **kwargs):
        self.server: str = ''
        self.port: int = 0
        self.is_ssl: bool = False
        self.sender: str = ''
        self.sender_alias: str = ''  # 发送人显示名
        self.password: str = ''
        self.receiver: List[str] = []  # 接收人邮件清单 , 分隔; aa@qq.com,bb@cc.com

        if kwargs:
            self.init(**kwargs)

    def init(self, server, sender, password, receiver, sender_alias='', port=25, is_ssl=False):
        self.server = server
        self.sender = sender
        self.sender_alias = sender_alias or sender
        self.password = password
        self.receiver = receiver
        self.port = port
        self.is_ssl = is_ssl

    def notify(self, title='邮件发送', content=''):
        # 消息
        message = MIMEText(content, 'plain', 'utf-8')
        message['From'] = utils.formataddr((self.sender_alias, self.sender))
        message['TO'] = Header(','.join(self.receiver), 'utf-8')
        message['Message-id'] = utils.make_msgid()
        message['Date'] = utils.formatdate()
        message['Subject'] = Header(title)

        # 登陆系统开始发送
        if self.is_ssl:  # 加密协议
            smtp = smtplib.SMTP_SSL(self.server, self.port)
        else:
            smtp = smtplib.SMTP(self.server, self.port)
        smtp.login(self.sender, self.password)
        smtp.sendmail(self.sender, self.receiver, message.as_string())
        smtp.quit()

    def verify(self, title='', content=''):
        title = title or '邮件通告'
        content = content or '邮件通告验证'
        return self.notify(title=title, content=content)


class WeChat(NotifyType):
    """微信发送"""

    Type = 'wechat'

    TO_ALL = '@all'
    AUTH_URL = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={corp_id}&corpsecret={secret_key}'  # 官网认证地址
    SEND_URL = 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}'  # 官方发送地址

    def __init__(self, **kwargs):
        self.agent_id = ''  # 发送账号
        self.corp_id = ''  # 企业ID
        self.secret_key = ''  # 密钥
        self.to_user: List[str] = []  # 接收人
        self.to_party: List[str] = []  # 接收群
        if kwargs:
            self.init(**kwargs)

    def init(self, agent_id, corp_id, secret_key, to_user, to_party):
        self.agent_id = agent_id
        self.corp_id = corp_id
        self.secret_key = secret_key
        self.to_user = to_user
        self.to_party = to_party

    def notify(self, title='', content='', url=''):
        if not (self.to_user or self.to_party):  # @所有人
            self.to_user = self.TO_ALL

        # 微信消息体
        message = dict(
            to_user=self.to_user,
            toparty=self.to_party,
            msgtype='textcard',
            agentid=self.agent_id,
            textcard=dict(title=title, description=content, url=url, btntxt='详情'),
            safe=0
        )

        resp = requests.get(self.AUTH_URL.format(corp_id=self.corp_id, secret_key=self.secret_key), verify=False)
        if resp.status_code != 200:
            raise Exception('微信Token获取失败')

        token = resp.json().get('access_token')
        if not token:
            raise Exception('微信Token提取失败')

        resp = requests.post(self.SEND_URL.format(token=token), data=json.dumps(message).encode('utf-8'),
                             headers={'Content-Type': 'application/json'}, verify=False)

        if resp.status_code != 200:
            raise Exception('微信消息发送失败')

        if resp.json().get('errcode') != 0:
            raise Exception('微信消息发送失败')

    def verify(self, title='', content=''):
        title = title or '微信通告'
        content = content or '微信通告验证'
        return self.notify(title=title, content=content)


class WebHook(NotifyType):
    """http通知"""

    Type = 'webhook'

    def __init__(self, **kwargs):
        self.url: str = ''
        self.method: str = ''
        if kwargs:
            self.init(**kwargs)

    def init(self, url, method):
        self.url = url
        self.method = method

    def notify(self, **kwargs):
        resp = requests.request(url=self.url, method=self.method, **kwargs)
        if resp.status_code != 200:
            raise Exception(resp.text)

    def verify(self, title='', content='', **kwargs):
        return self.notify(**kwargs)


class Script:
    Type = 'script'

    def __init__(self, category, script, variables):
        self.script = script
        self.category = category
        self.variables = variables

    def execute_python(self):
        """python脚本执行"""
        exec(self.script, self.variables)
        return 0

    def execute_shell(self):
        variable_str = '\n'.join([f'{k}={json.dumps(v, ensure_ascii=False)}' for k, v in self.variables.items()])
        script = '\n'.join([variable_str, self.script])
        resp = subprocess.run(['sh', '-c', script], capture_output=True, text=True, timeout=10)
        if resp.returncode == 0:
            print(resp.stdout)
            return 0
        raise Exception(resp.stderr)

    def execute_perl(self):
        """perl"""
        variable_str = '\n'.join([f'${k}={json.dumps(v, ensure_ascii=False)};' for k, v in self.variables.items()])
        script = '\n'.join([variable_str, self.script])
        resp = subprocess.run(['perl', '-e', script], capture_output=True, text=True, timeout=10)
        if resp.returncode == 0:
            print(resp.stdout)
            return 0
        raise Exception(resp.stderr)

    def notify(self):
        executor = getattr(self, f'execute_{self.category.lower()}')
        if executor:
            executor()

    def verify(self):
        return self.notify()


class SysLog(NotifyType):

    Type = 'syslog'

    def __init__(self, **kwargs):
        self.server = None
        self.port = None
        self.proto = None
        if kwargs:
            self.init(**kwargs)

    def init(self, server, port: int, proto):
        self.server = server
        self.port = port
        self.proto = proto

    def notify(self, content, facility=1, severity=1, **kwargs):
        """单条发送"""
        client = pysyslogclient.SyslogClientRFC5424(server=self.server, port=self.port, proto=self.proto)
        client.log(content, severity=severity, facility=facility, **kwargs)
        client.close()

    def verify(self, content=''):
        content = content or 'Syslog通告测试'
        return self.notify(content)


NotifyMap = {cls.Type: cls for cls in NotifyType.__subclasses__()}


if __name__ == '__main__':
    # 脚本执行
    s = Script(category='python', script='print(f"hello {a}")', variables={'a': 'python script'})
    s.notify()
    s = Script(category='shell', script='echo "hello ${a}"', variables={'a': 'shell script'})
    s.notify()
    s = Script(category='perl', script='print "hello ${a}";', variables={'a': 'perl script'})
    s.notify()
